// Copyright (c) 2025 Shenzhen Kaihong Digital Industry Development Co., Ltd.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::{fmt::Display, path::{Path, PathBuf}};

use clap::ValueEnum;
use log::trace;

use crate::{config::pkg_config::{DeployConfig, NoDeployCrossCompileIsolation}, database::local::PKGS_ROOT, utils::{copy_dir_all, hard_link_dir_all, symlink_dir_all}, Arch};

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum MergeMode {
    /// Copy all files, slow
    Copy,
    /// Fast, but may cause build error
    Symlink,
    HardLink,
}

pub enum MergeRoot {
    Normal,
    Host,
    Cross(Arch),
    HostNoIsolation(Vec<String>), 
}

#[derive(Debug, thiserror::Error)]
#[error("failed to {mode} {src} into {dst}: {cause}")]
pub struct MergeError {
    mode: MergeMode,
    src: PathBuf,
    dst: PathBuf,
    cause: std::io::Error,
}

impl MergeMode {
    pub fn as_str(&self) -> &'static str {
        match self {
            MergeMode::Copy => "copy",
            MergeMode::Symlink => "symlink",
            MergeMode::HardLink => "hardlink",
        }
    }
}

impl Display for MergeMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

impl TryFrom<u8> for MergeMode {
    type Error = u8;

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        match value {
            0 => Ok(MergeMode::Copy),
            1 => Ok(MergeMode::Symlink),
            2 => Ok(MergeMode::HardLink),
            _ => Err(value),
        }
    }
}

impl MergeRoot {
    pub fn new(deploy_config: Option<&DeployConfig>, spec_arch: Arch, arch: Arch) -> Self {
        let node_arch = if deploy_config.as_ref().map(|c| c.as_host).unwrap_or_default() {
            Arch::host()
        } else {
            spec_arch
        };
        if node_arch.is_host() && !arch.is_host() {
            if let Some(deploy_config) = deploy_config {
                match &deploy_config.no_cross_compile_isolation {
                    NoDeployCrossCompileIsolation::None => Self::Host,
                    NoDeployCrossCompileIsolation::All => Self::Normal,
                    NoDeployCrossCompileIsolation::Paths(paths) => Self::HostNoIsolation(paths.clone()),
                }
            } else {
                Self::Host
            }
        } else if node_arch == arch {
            Self::Normal
        } else {
            Self::Cross(node_arch)
        }
    }
}

pub fn merge_pkg(
    root: &Path,
    deploy_config: Option<&DeployConfig>,
    node_arch: Arch,
    spec_str: &str,
    arch: Arch,
    mode: MergeMode,
) -> Result<(), MergeError> {
    match MergeRoot::new(deploy_config, node_arch, arch) {
        MergeRoot::Normal => merge_normal_pkg(root, spec_str, mode)?,
        MergeRoot::Host => merge_host_pkg(root, spec_str, mode)?,
        MergeRoot::Cross(node_arch) => merge_cross_pkg(root, spec_str, mode, node_arch)?,
        MergeRoot::HostNoIsolation(paths) => {
            merge_host_pkg(root, spec_str, mode)?;
            merge_host_no_isolation_pkg(root, spec_str, mode, &paths)?
        }
    }
    Ok(())
}

fn merge_host_no_isolation_pkg(
    root: &Path,
    spec_str: &str,
    mode: MergeMode,
    paths: &Vec<String>,
) -> Result<(), MergeError> {
    for path in paths {
        let from = PKGS_ROOT.join(spec_str).join(path);
        if !from.exists() {
            continue;
        }

        let to = root.join(path);
        
        if from.is_dir() {
            merge_dir_all(&from, &to, mode)?;
        } else if from.is_symlink() {
            let linkto = std::fs::read_link(&from).map_err(|e| MergeError {
                mode,
                src: from.clone(),
                dst: to.clone(),
                cause: e,
            })?;
            std::os::unix::fs::symlink(linkto, &to).map_err(|e| MergeError {
                mode,
                src: from,
                dst: to,
                cause: e,
            })?;
        } else {
            merge_file(&from, &to, mode)?;
        }
    }
    Ok(())
}

fn merge_normal_pkg(
    root: &Path,
    spec_str: &str,
    mode: MergeMode,
) -> Result<(), MergeError> {
    let src = PKGS_ROOT.join(spec_str);
    merge_dir_all(src.as_path(), root, mode)
}

fn merge_host_pkg(
    root: &Path,
    spec_str: &str,
    mode: MergeMode,
) -> Result<(), MergeError> {
    let src = PKGS_ROOT.join(spec_str);
    let dst = root.join("host");
    merge_dir_all(src, dst, mode)
}

fn merge_cross_pkg(
    root: &Path,
    spec_str: &str,
    mode: MergeMode,
    arch: Arch,
) -> Result<(), MergeError> {
    let src = PKGS_ROOT.join(spec_str);
    let dst = root.join("cross").join(arch.as_str());
    merge_dir_all(src, dst, mode)
}

pub fn merge_dir_all<P: AsRef<Path>>(src: P, dst: P, mode: MergeMode) -> Result<(), MergeError>  {
    let src = src.as_ref();
    let dst = dst.as_ref();
    trace!("Merging {} into {}", src.display(), dst.display());
    let res = match mode {
        MergeMode::Copy => copy_dir_all(src, dst),
        MergeMode::Symlink => symlink_dir_all(src, dst),
        MergeMode::HardLink => hard_link_dir_all(src, dst),
    };
    res.map_err(|e| MergeError {
        mode,
        src: src.to_path_buf(),
        dst: dst.to_path_buf(),
        cause: e,
    })
}

fn merge_file<P: AsRef<Path>>(src: P, dst: P, mode: MergeMode) -> Result<(), MergeError> {
    let src = src.as_ref();
    let dst = dst.as_ref();
    let res = match mode {
        MergeMode::Copy => std::fs::copy(src, dst).map(|_| ()),
        MergeMode::Symlink => std::os::unix::fs::symlink(src, dst),
        MergeMode::HardLink => std::fs::hard_link(src, dst),
    };
    res.map_err(|e| MergeError {
        mode,
        src: src.to_path_buf(),
        dst: dst.to_path_buf(),
        cause: e,
    })
}